package com.kornero.util.sensor.accelerometr;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;

import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

import static com.kornero.util.sensor.accelerometr.MathHelper.*;

/**
 * First of all attach this class to the sensors in your activity:
 * <pre>
 * protected void onPause() {
 *
 *     // Stop updates to save power while app paused.
 *     Rotation.getInstance().onPause(this);
 * }
 *
 * protected void onResume() {
 *
 *     // Start updates for one or more sensors.
 *     Rotation.getInstance().onResume(this);
 * }
 * </pre>
 * <p/>
 * You can get smoothed accelerometer values using:
 * {@link #getCurrentValues()}}.
 * <p/>
 * Or you can attach your listener and get accelerometer changes:
 * <pre>
 * class Class implements Observer {
 *
 *     Class() {
 *         Rotation.getInstance().addObserver(this);
 *     }
 *
 *     public void update(Observable observable, Object data) {
 *         float[] delta = (float[]) data;
 *         float dx = delta[Rotation.X];
 *         float dy = delta[Rotation.Y];
 *     }
 * }
 * </pre>
 */
public class Rotation extends Observable implements SensorEventListener {

    // Constants.
    public static final int DIMENSION = 3;
    public static final int X = 0;
    public static final int Y = 1;
    public static final int Z = 2;

    private static final String TAG = Rotation.class.getSimpleName();
    private static final Rotation SINGLETON = new Rotation();

    private static final int QUEUE_MAX_SIZE = 5;
    private static final int MULTIPLEXER = 8;
    private static final float THRESHOLD = 0.1f;

    private static final List<Integer> SENSOR_TYPES = Arrays.asList(
            Sensor.TYPE_ACCELEROMETER
//            ,Sensor.TYPE_MAGNETIC_FIELD
//            ,Sensor.TYPE_GYROSCOPE
//            ,Sensor.TYPE_GRAVITY
    );

    private final Map<Integer, Queue<Float>> valuesHistory = Collections.unmodifiableMap(
            new HashMap<Integer, Queue<Float>>() {{
                put(X, new ConcurrentLinkedQueue<Float>());
                put(Y, new ConcurrentLinkedQueue<Float>());
                put(Z, new ConcurrentLinkedQueue<Float>());
            }}
    );

    private final float[] previousValues = new float[DIMENSION];

    private Rotation() {
    }

    public static synchronized Rotation getInstance() {
        return SINGLETON;
    }

    public float[] getCurrentValues() {
        final float[] values = new float[DIMENSION];
        values[X] = average(valuesHistory.get(X), MULTIPLEXER);
        values[Y] = average(valuesHistory.get(Y), MULTIPLEXER);
        values[Z] = average(valuesHistory.get(Z), MULTIPLEXER);
        return values;
    }

    public void onPause(final Context context) {
        final SensorManager mgr = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        mgr.unregisterListener(this);
    }

    public void onResume(final Context context) {
        final SensorManager mgr = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);

        // Start updates for one or more sensors.
        for (final Integer sensorType : SENSOR_TYPES) {
            final List<Sensor> sensorList = mgr.getSensorList(sensorType);
            if (sensorList.isEmpty()) {
                Log.w(TAG, "No " + sensorType + ": " + mgr + "; " + context);
            }
            for (final Sensor sensor : sensorList) {
                mgr.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
            }
        }
    }

    @Override
    public void onSensorChanged(final SensorEvent event) {
        printDebugInfo(event);
        this.processValues(event.values);
        final float[] deltaValues = calcDelta(this.previousValues, event.values);

        Log.d(TAG, "onSensorChanged():" + Arrays.toString(deltaValues));

        printDirection(deltaValues);

        // Update listeners.
        setChanged();
        notifyObservers(deltaValues);

        // Save current values.
        System.arraycopy(event.values, 0, previousValues, 0, DIMENSION);
    }

    @Override
    public void onAccuracyChanged(final Sensor sensor, final int accuracy) {
        Log.d(TAG, "onAccuracyChanged():sensor=" + sensor + ", accuracy=" + accuracy);
    }

    private void processValues(final float[] values) {
        addIntoQueue(valuesHistory.get(X), values[X]);
        addIntoQueue(valuesHistory.get(Y), values[Y]);
        addIntoQueue(valuesHistory.get(Z), values[Z]);
    }

    private static void addIntoQueue(final Queue<Float> floats, final float value) {
        addIntoQueue(floats, value, QUEUE_MAX_SIZE);
    }

    private static void addIntoQueue(final Queue<Float> floats, final float value, final int queueMaxSize) {
        while (floats.size() >= queueMaxSize) {
            floats.poll();
        }
        floats.offer(value);
    }

    private static void printDebugInfo(final SensorEvent event) {
        switch (event.sensor.getType()) {
            case Sensor.TYPE_ACCELEROMETER:
                Log.v(TAG, "TYPE_ACCELEROMETER:" + Arrays.toString(event.values));
                break;
            case Sensor.TYPE_MAGNETIC_FIELD:
                Log.v(TAG, "TYPE_MAGNETIC_FIELD:" + Arrays.toString(event.values));
                break;
            case Sensor.TYPE_GRAVITY:
                Log.v(TAG, "TYPE_GRAVITY:" + Arrays.toString(event.values));
                break;
            case Sensor.TYPE_GYROSCOPE:
                Log.v(TAG, "TYPE_GYROSCOPE:" + Arrays.toString(event.values));
                break;
        }
    }

    private static void printDirection(final float[] deltaValues) {
        boolean left = false,
                right = false,
                up = false,
                down = false;

        final double deltaSqrt = calcArraySqrt(deltaValues);
        if (deltaSqrt > THRESHOLD) {
            final float absX = Math.abs(deltaValues[X]);
            final float absY = Math.abs(deltaValues[Y]);
            if (absX > THRESHOLD && absX > absY) {
                if (deltaValues[X] > 0.0f) {
                    right = true;
                } else {
                    left = true;
                }
            }
            if (absY > THRESHOLD && absY > Math.abs(deltaValues[Z])) {
                if (deltaValues[Y] > 0.0f) {
                    up = true;
                } else {
                    down = true;
                }
            }
            Log.v(TAG, "printDirection():"
                            + (right ? ">" : "")
                            + (left ? "<" : "")
                            + (up ? "^" : "")
                            + (down ? "v" : "")
            );
        }
    }
}